我是怎么把业务代码越写越复杂的?
本文作者
作者:唐子玄
链接:
https://juejin.im/post/6844904176296673287
本文由作者授权发布。
稳住今天是周末,给大家推一篇值得思考和品味的文章。
本文以一个真实项目的业务场景为载体,描述了经历一次次重构后,代码变得越来越复杂(you ya)的过程。
本篇 Demo 的业务场景是:从服务器拉取新闻并在列表展示。
刚接触 Android 时,我是这样写业务代码的(省略了和主题无关的 Adapter 和 Api 细节):
class GodActivity : AppCompatActivity() {
private var rvNews: RecyclerView? = null
private var newsAdapter = NewsAdapter()
// 用 retrofit 拉取数据
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.apiopen.top")
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient.Builder().build())
.build()
private val newsApi = retrofit.create(NewsApi::class.java)
// 数据库操作异步执行器
private var dbExecutor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.news_activity)
initView()
fetchNews()
}
private fun initView() {
rvNews = findViewById(R.id.rvNews)
rvNews?.layoutManager = LinearLayoutManager(this)
}
// 列表展示新闻
private fun showNews(news : List<News>) {
newsAdapter.news = news
rvNews?.adapter = newsAdapter
}
// 获取新闻
private fun fetchNews() {
// 1. 先从数据库读老新闻以快速展示
queryNews().let{ showNews(it) }
// 2. 再从网络拉新闻替换老新闻
newsApi.fetchNews(
mapOf("page" to "1","count" to "4")
).enqueue(object : Callback<NewsBean> {
override fun onFailure(call: Call<NewsBean>, t: Throwable) {
Toast.makeText(this@GodActivity, "network error", Toast.LENGTH_SHORT).show()
}
override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {
response.body()?.result?.let {
// 3. 展示新新闻
showNews(it)
// 4. 将新闻入库
dbExecutor.submit { insertNews(it) }
}
}
})
}
// 从数据库读老新闻(伪代码)
private fun queryNews() : List<News> {
val dbHelper = NewsDbHelper(this, ...)
val db = dbHelper.getReadableDatabase()
val cursor = db.query(...)
var newsList = mutableListOf<News>()
while(cursor.moveToNext()) {
...
newsList.add(news)
}
db.close()
return newsList
}
// 将新闻写入数据库(伪代码)
private fun insertNews(news : List<News>) {
val dbHelper = NewsDbHelper(this, ...)
val db = dbHelper.getWriteableDatabase()
news.foreach {
val cv = ContentValues().apply { ... }
db.insert(cv)
}
db.close()
}
}
毕竟当时的关注点是实现功能,首要解决的问题是“如何绘制布局”、“如何操纵数据库”、“如何请求并解析网络数据”、“如何将数据填充在列表中”。待这些问题解决后,也没时间思考架构,所以就产生了上面的God Activity。Activity 管的太多了!Activity 知道太多细节:
异步细节 访问数据库细节 访问网络细节
拿说话打个比方:
你问 “晚饭吃了啥?”
“我用勺子一口一口地吃了鸡生下的蛋和番茄再加上油一起炒的菜。”
听了这样地回答,你还会和他做朋友吗?其实你并不关心他吃的工具、吃的速度、食材的来源,以及烹饪方式。
比如 “异步” 在 Android 中就有好几种实现方式:线程池、HandlerThread、协程、IntentService、RxJava。
GodActivity 引入了大量本和它无关的类:Retrofit、Executors、ContentValues、Cursor、SQLiteDatabase、Response、OkHttpClient。Activity 本应该只和界面展示有关。
既然 Activity 知道太多,那就让Presenter来为它分担:
// 构造 Presenter 时传入 view 层接口 NewsView
class NewsPresenter(var newsView: NewsView): NewsBusiness {
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.apiopen.top")
.addConverterFactory(MoshiConverterFactory.create())
.client(OkHttpClient.Builder().build())
.build()
private val newsApi = retrofit.create(NewsApi::class.java)
private var executor = Executors.newSingleThreadExecutor()
override fun fetchNews() {
// 将数据库新闻通过 view 层接口通知 Activity
queryNews().let{ newsView.showNews(it) }
newsApi.fetchNews(
mapOf("page" to "1", "count" to "4")
).enqueue(object : Callback<NewsBean> {
override fun onFailure(call: Call<NewsBean>, t: Throwable) {
newsView.showNews(null)
}
override fun onResponse(call: Call<NewsBean>, response: Response<NewsBean>) {
response.body()?.result?.let {
// 将网络新闻通过 view 层接口通知 Activity
newsView.showNews(it)
dbExecutor.submit { insertNews(it) }
}
}
})
}
// 从数据库读老新闻(伪代码)
private fun queryNews() : List<News> {
// 通过 view 层接口获取 context 构造 dbHelper
val dbHelper = NewsDbHelper(newsView.newsContext, ...)
val db = dbHelper.getReadableDatabase()
val cursor = db.query(...)
var newsList = mutableListOf<News>()
while(cursor.moveToNext()) {
...
newsList.add(news)
}
db.close()
return newsList
}
// 将新闻写入数据库(伪代码)
private fun insertNews(news : List<News>) {
val dbHelper = NewsDbHelper(newsView.newsContext, ...)
val db = dbHelper.getWriteableDatabase()
news.foreach {
val cv = ContentValues().apply { ... }
db.insert(cv)
}
db.close()
}
}
无非就是复制 + 粘贴,把 GodActivity 中的“异步”、“访问数据库”、“访问网络”、放到了一个新的Presenter类中。
这样 Activity 就变简单了:
class RetrofitActivity : AppCompatActivity(), NewsView {
// 在界面中直接构造业务接口实例
private val newsBusiness = NewsPresenter(this)
private var rvNews: RecyclerView? = null
private var newsAdapter = NewsAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.news_activity)
initView()
// 触发业务逻辑
newsBusiness.fetchNews()
}
private fun initView() {
rvNews = findViewById(R.id.rvNews)
rvNews?.layoutManager = LinearLayoutManager(this)
}
// 实现 View 层接口以更新界面
override fun showNews(news: List<News>?) {
newsAdapter.news = news
rvNews?.adapter = newsAdapter
}
override val newsContext: Context
get() = this
}
Presenter的引入还增加了通信成本:
interface NewsBusiness {
fun fetchNews()
}
这是MVP模型中的业务接口,描述的是业务动作。它由Presenter实现,而界面类持有它以触发业务逻辑。
interface NewsView {
// 将新闻传递给界面
fun showNews(news:List<News>?)
// 获取界面上下文
abstract val newsContext:Context
}
在MVP模型中,这称为View 层接口。Presenter持有它以触发界面更新,而界面类实现它以绘制界面。
这两个接口的引入,意义非凡:
接口把 做什么(抽象) 和 怎么做(细节) 分离。这个特性使得 关注点分离 成为可能:接口持有者只关心 做什么,而 怎么做 留给接口实现者关心。
Activity 持有业务接口,这使得它不需要关心业务逻辑的实现细节。Activity 实现View 层接口,界面展示细节都内聚在 Activity 类中,使其成为MVP中的V。
Presenter 持有View 层接口,这使得它不需要关心界面展示细节。Presenter 实现业务接口,业务逻辑的实现细节都内聚在 Presenter 类中,使其成为MVP中的P。
这样做最大的好处是降低代码理解成本,因为不同细节不再是在同一层次被铺开,而是被分层了。阅读代码时,“浅尝辄止”或“不求甚解”的阅读方式极大的提高了效率。
这样做还能缩小变更成本,业务需求发生变更时,只有Presenter类需要改动。界面调整时,只有V层需要改动。同理,排查问题的范围也被缩小。
这样还方便了自测,如果想测试各种临界数据产生时界面的表现,则可以实现一个PresenterForTest。如果想覆盖业务逻辑的各种条件分支,则可以方便地给Presenter写单元测试(和界面隔离后,Presenter 是纯 Kotlin 的,不含有任何 Android 代码)。
但NewsPresenter也不单纯!它除了包含业务逻辑,还包含了访问数据的细节,应该用同样的思路,抽象出一个访问数据的接口,让Presenter持有,这就是MVP中的M。它的实现方式可以参考下一节的Repository。
即使将访问数据的细节剥离出Presenter,它依然不单纯。因为它持有View 层接口,这就要求Presenter需了解 该把哪个数据传递给哪个接口方法,这就是 数据绑定,它在构建视图时就已经确定(无需等到数据返回),所以这个细节可以从业务层剥离,归并到视图层。
Presenter的实例被 Activity 持有,所以它的生命周期和 Activiy 同步,即业务数据和界面同生命周期。在某些场景下,这是一个缺点,比如横竖屏切换。此时,如果数据的生命周期不依赖界面,就可以免去重新获取数据的成本。这势必 需要一个生命周期更长的对象(ViewModel)持有数据。
生命周期更长的 ViewModel
上一节的例子中,构建Presenter是直接在Activity中new,而构建ViewModel是通过ViewModelProvider.get():
public class ViewModelProvider {
// ViewModel 实例商店
private final ViewModelStore mViewModelStore;
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
// 从商店获取 ViewModel实例
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
} else {
...
}
// 若商店无 ViewModel 实例 则通过 Factory 构建
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
// 将 ViewModel 实例存入商店
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
}
ViewModel实例通过ViewModelStore获取:
// ViewModel 实例商店
public class ViewModelStore {
// 存储 ViewModel 实例的 Map
private final HashMap<String, ViewModel> mMap = new HashMap<>();
// 存
final void put(String key, ViewModel viewModel) {
ViewModel oldViewModel = mMap.put(key, viewModel);
if (oldViewModel != null) {
oldViewModel.onCleared();
}
}
// 取
final ViewModel get(String key) {
return mMap.get(key);
}
...
}
ViewModelStore将ViewModel实例存储在HashMap中。
而ViewModelStore通过ViewModelStoreOwner获取:
public class ViewModelProvider {
// ViewModel 实例商店
private final ViewModelStore mViewModelStore;
// 构造 ViewModelProvider 时需传入 ViewModelStoreOwner 实例
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
// 通过 ViewModelStoreOwner 获取 ViewModelStore
this(owner.getViewModelStore(), factory);
}
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
}
那ViewModelStoreOwner实例又存储在哪?
// Activity 基类实现了 ViewModelStoreOwner 接口
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner {
// Activity 持有 ViewModelStore 实例
private ViewModelStore mViewModelStore;
public ViewModelStore getViewModelStore() {
if (mViewModelStore == null) {
// 获取配置无关实例
NonConfigurationInstances nc =(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// 从配置无关实例中恢复 ViewModel商店
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
// 静态的配置无关实例
static final class NonConfigurationInstances {
// 持有 ViewModel商店实例
ViewModelStore viewModelStore;
...
}
}
Activity就是ViewModelStoreOwner实例,且持有ViewModelStore实例,该实例还会被保存在一个静态类中,所以ViewModel生命周期比Activity更长。这样 ViewModel 中存放的业务数据就可以在Activity销毁重建时被复用。
数据绑定
MVVM中Activity 属于V层,布局构建以及数据绑定都在这层完成:
class MvvmActivity : AppCompatActivity() {
private var rvNews: RecyclerView? = null
private var newsAdapter = NewsAdapter()
// 构建布局
private val rootView by lazy {
ConstraintLayout {
TextView {
layout_id = "tvTitle"
layout_width = wrap_content
layout_height = wrap_content
textSize = 25f
padding_start = 20
padding_end = 20
center_horizontal = true
text = "News"
top_toTopOf = parent_id
}
rvNews = RecyclerView {
layout_id = "rvNews"
layout_width = match_parent
layout_height = wrap_content
top_toBottomOf = "tvTitle"
margin_top = 10
center_horizontal = true
}
}
}
// 构建 ViewModel 实例
private val newsViewModel by lazy {
// 构造 ViewModelProvider 实例, 通过其 get() 获得 ViewModel 实例
ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(rootView)
initView()
bindData()
}
// 将数据绑定到视图
private fun bindData() {
newsViewModel.newsLiveData.observe(this, Observer {
newsAdapter.news = it
rvNews?.adapter = newsAdapter
})
}
private fun initView() {
rvNews?.layoutManager = LinearLayoutManager(this)
}
}
其中构建布局 DSL 的详细介绍可以点击这里。
它省去了原先V层( Activity + xml )中的xml。
代码中的数据绑定是通过观察ViewModel中的LiveData实现的。这不是数据绑定的完全体,所以还需手动地观察observe数据变化(只有当引入data-binding包后,才能把视图和控件的绑定都静态化到 xml 中)。
但至少它让ViewModel无需主动推数据了:
在 MVP 模式中,Presenter持有View 层接口并主动向界面推数据。
MVVM模式中,ViewModel不再持有View 层接口,也不主动给界面推数据,而是界面被动地观察数据变化。
这使得ViewModel只需持有数据并根据业务逻辑更新之即可:
// 数据访问接口在构造函数中注入
class NewsViewModel(var newsRepository: NewsRepository) : ViewModel() {
// 持有业务数据
val newsLiveData by lazy { newsRepository.fetchNewsLiveData() }
}
// 定义构造 ViewModel 方法
class NewsFactory(context: Context) : ViewModelProvider.Factory {
// 构造 数据访问接口实例
private val newsRepository = NewsRepositoryImpl(context)
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
// 将数据接口访问实例注入 ViewModel
return NewsViewModel(newsRepository) as T
}
}
// 然后就可以在 Activity 中这样构造 ViewModel 了
class MvvmActivity : AppCompatActivity() {
// 构建 ViewModel 实例
private val newsViewModel by lazy {
ViewModelProvider(this, NewsFactory(applicationContext)).get(NewsViewModel::class.java) }
}
ViewModel只关心业务逻辑和数据,不关心获取数据的细节,所以它们都被数据访问接口隐藏了。
Demo 业务场景中,ViewModel 只有一行代码,那它还有存在的价值吗?
有!即使在业务逻辑如此简单的场景下还是有!因为ViewModel生命周期比 Activity 长,其持有的数据可以在 Activity 销毁重建时复用。
真实项目中的业务逻辑复杂度远高于 Demo,应该将业务逻辑的细节隐藏在ViewModel中,让界面类无感知。比如 “将服务器返回的时间戳转化成年月日” 就应该写在ViewModel中。
业务数据访问接口
// 业务数据访问接口
interface NewsRepository {
// 拉取新闻并以 LiveData 方式返回
fun fetchNewsLiveData():LiveData<List<News>?>
}
// 实现访问网络和数据库的细节
class NewsRepositoryImpl(context: Context) : NewsRepository {
// 使用 Retrofit 构建请求访问网络
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.apiopen.top")
.addConverterFactory(MoshiConverterFactory.create())
// 将返回数据组织成 LiveData
.addCallAdapterFactory(LiveDataCallAdapterFactory())
.client(OkHttpClient.Builder().build())
.build()
private val newsApi = retrofit.create(NewsApi::class.java)
private var executor = Executors.newSingleThreadExecutor()
// 使用 room 访问数据库
private var newsDatabase = NewsDatabase.getInstance(context)
private var newsDao = newsDatabase.newsDao()
private var newsLiveData = MediatorLiveData<List<News>>()
override fun fetchNewsLiveData(): LiveData<List<News>?> {
// 从数据库获取新闻
val localNews = newsDao.queryNews()
// 从网络获取新闻
val remoteNews = newsApi.fetchNewsLiveData(
mapOf("page" to "1", "count" to "4")
).let {
Transformations.map(it) { response: ApiResponse<NewsBean>? ->
when (response) {
is ApiSuccessResponse -> {
val news = response.body.result
news?.let {
// 将网络新闻入库
executor.submit { newsDao.insertAll(it) }
}
news
}
else -> null
}
}
}
// 将数据库和网络响应的 LiveData 合并
newsLiveData.addSource(localNews) {
newsLiveData.value = it
}
newsLiveData.addSource(remoteNews) {
newsLiveData.value = it
}
return newsLiveData
}
}
这就是MVVM中的M,它定义了如何获取数据的细节。
Demo 中 数据库和网络都返回 LiveData 形式的数据,这样合并两个数据源只需要一个MediatorLiveData。所以使用了 Room 来访问数据库。并且定义了LiveDataCallAdapterFactory用于将 Retrofit 返回结果也转化成 LiveData。(其源码可以在这里找到)
https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/util/LiveDataCallAdapterFactory.kt
这里也存在耦合:Repository需要了解 Retrofit 和 Room 的使用细节。
当访问数据库和网络的细节越来越复杂,甚至又加入内存缓存时,再增加一层抽象,分别把访问内存、数据库、和网络的细节都隐藏起来,也是常见的做法。这样Repository中的逻辑就变成:“运用什么策略将内存、数据库和网络的数据进行组合并返回给业务层”。
经多次重构,代码结构不断衍化,最终引入了ViewModel和Repository。层次变多了,表面上看是越来越复杂了,但其实理解成本越来越低。因为 所有复杂的细节并不是在同一层次被展开。
最后用 Clean architecture 再审视一下这套架构:
Entities
它是业务实体对象,对于 Demo 来说 Entities 就是新闻实体类News。
Use Cases
它是业务逻辑,Entities 是名词,Use Cases 就是用它造句。对于 Demo 来说 Use Cases 就是 “展示新闻列表” 在 Clean Architecture 中每一个业务逻辑都会被抽象成一个 UseCase 类,它被Presenters持有,详情可以去这里了解
Repository
它是业务数据访问接口,抽象地描述获取和存储 Entities。和 Demo 中的 Repository 一模一样,但在 Clean Architecture 中,它由 UseCase 持有。
Presenters
它和MVP模型中 Presenter 几乎一样,由它触发业务逻辑,并把数据传递给界面。唯一的不同是,它持有 UseCase。
DB & API
它是抽象业务数据访问接口的实现,和 Demo 中的NewsRepositoryImpl一模一样。
UI
它是构建布局的细节,就像 Demo 中的 Activity。
Device
它是和设备相关的细节,DB 和 UI 的实现细节也和设备有关,这里的 Device是指除了数据和界面之外的和设备相关的细节,比如如何在通知栏展示通知。
依赖方向
洋葱圈的内三层都是抽象,而只有最外层才包含实现细节(和 Android 平台相关的实现细节。比如访问数据库的细节、绘制界面的细节、通知栏提醒消息的细节、播放音频的细节)
洋葱圈向内的箭头意思是:外层知道相邻内层的存在,而内层不知道外层的存在。即外层依赖内层,内层不依赖外层。也就说应该尽可能把业务逻辑抽象地实现,业务逻辑只需要关心做什么,而不该关心怎么做。这样的代码对扩展友好,当实现细节变化时,业务逻辑不需要变。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!